<?php
/*
 * Quark PHP Framework
 * Copyright 2011, Sahib Alejandro Jaramillo Leo.
 * http://quarkphp.com/
 * Licensed under the GNU General Public License (http://www.gnu.org/licenses/gpl.html).
 */
/**
 * Clase para manejar uploads
 * @author sahib
 */
class FileUploader {

	/**
	 * @see FileUploader::setOverwrite()
	 * @var bool
	 */
	protected $overwrite;

	/**
	 * @see FileUploader::setExtensions()
	 * @var array
	 */
	protected $extensions;

	/**
	 * @see FileUploader::__construct() 
	 * @var string
	 */
	protected $upload_path;

	/**
	 * @see FileUploader::__construct()
	 * @var string
	 */
	protected $input_file_name;

	/**
	 * @see FileUploader::setFileNameMaxSize()
	 * @var int
	 */
	protected $file_name_max_size;

	/**
	 * Flag
	 * @see FileUploader::setIgnoreNoFile()
	 * @var bool
	 */
	protected $ignore_no_file;

	/**
	 * @see FileUploader::setChmodMode()
	 * @var octal
	 */
	protected $chmod_mode;

	/**
	 * Constructor
	 * 
	 * @param string $input_file_name Nombre del input file en el formulario HTML.
	 * @param string $upload_path Directorio destino, relativo al script en ejecución.
	 * @throws Exception Si el directorio destino no existe o no se puede escribir en el.
	 */
	public function __construct( $input_file_name, $upload_path ) {

		$this->overwrite = FALSE;
		$this->extensions = NULL;
		$this->chmod_mode = 0777;
		$this->ignore_no_file = FALSE;
		$this->file_name_max_size = 255;
		$this->input_file_name = $input_file_name;
		
		// Agregamos tail slash
		$this->setUploadPath($upload_path);
		
		if( !is_dir( $this->upload_path ) or !is_writable( $this->upload_path ) )
			throw new Exception( __METHOD__ . ' Directorio "' . $this->upload_path . '" no existe o no tiene permisos de escritura.', 1 );
	}

	/**
	 * Copia todos los archivos al directorio destino,
	 * si es un upload multiple devuelve un array de objetos,
	 * si es un upload monogamo devuelve un objeto,
	 * El(los) objeto(s) devuelto(s) contiene(n) las siguientes propiedades:
	 * error[string]:
	 * Mensaje de error en caso de ocurrir alguno, por default es FALSE,
	 * final_file_name[string]:
	 * Nombre final del archivo copiado,
	 * file_name[string]:
	 * Nombre original del archivo enviado.
	 * 
	 * @param $input_file_name Nombre del INPUT FILE enviado
	 * @param $final_file_name Nombre que se le dará al archivo copiado (sin extension)
	 * 
	 * @return Object|array(Object|NULL,...)|NULL
	 */
	public function upload( $input_file_name = NULL, $final_file_name = NULL ) {

		$input_file_name_bak = $this->input_file_name;
		
		if( $input_file_name != NULL ) {
			$this->input_file_name = $input_file_name;
		}
		
		if( empty( $_FILES ) ) {
			throw new Exception( '$_FILES esta vacio', 1 );
		}
		else {
			$F = &$_FILES[$this->input_file_name];
			
			if( is_array( $F['name'] ) ) {
				// Upload multiple
				$uploads = array();
				foreach( $F['name'] as $i => $file_name ) {
					if( $F['error'][$i] != UPLOAD_ERR_NO_FILE or ($this->ignore_no_file == FALSE) ) {
						$uploads[] = $this->moveFile( $i, $final_file_name );
					}
				}
				
				$this->input_file_name = $input_file_name_bak;
				return $uploads;
			}
			else {
				
				// Upload normal
				if( $F['error'] != UPLOAD_ERR_NO_FILE or ($this->ignore_no_file == FALSE) ) {
					
					$Upload = $this->moveFile( NULL, $final_file_name );
					$this->input_file_name = $input_file_name_bak;
					return $Upload;
				}
			}
		}
	}

	/**
	 * Realiza la tarea del copiado de archivos, es como un "move_uploaded_file()" pero con esteroides,
	 * devuelve un objeto con informacion sobre el copiado.
	 *
	 * @see FileUploader::upload()
	 * @param int $i Indice del archivo a copiar (en caso de ser upload multiple)
	 * @return Object
	 */
	protected function moveFile( $i = NULL, $final_file_name = NULL ) {

		$F = &$_FILES[$this->input_file_name];
		
		if( $i !== NULL ) {
			$error = $F['error'][$i];
			$file_name = $F['name'][$i];
			$source_file_path = $F['tmp_name'][$i];
		}
		else {
			$error = $F['error'];
			$file_name = $F['name'];
			$source_file_path = $F['tmp_name'];
		}
		
		/*
		 * Resultado para el return
		 */
		$UploadInfo = new stdClass( );
		
		$UploadInfo->error = FALSE;
		$UploadInfo->final_file_name = NULL; // Asignado más abajo
		$UploadInfo->file_name = $file_name;
		
		/*
		 * Mover archivo solo si subio correctamente
		 */
		if( $error != UPLOAD_ERR_OK ) {
			$UploadInfo->error = 'Error al enviar archivo, ';
			switch( $error ) {
				case UPLOAD_ERR_INI_SIZE:
					$UploadInfo->error .= 'el tamaño excede el limite.';
					break;
				case UPLOAD_ERR_FORM_SIZE:
					$UploadInfo->error .= 'el tamaño excede el limite.';
					break;
				case UPLOAD_ERR_PARTIAL:
					$UploadInfo->error .= 'envio incompleto.';
					break;
				case UPLOAD_ERR_NO_FILE:
					$UploadInfo->error .= 'no se envió el archivo.';
					break;
				case UPLOAD_ERR_NO_TMP_DIR:
					$UploadInfo->error .= 'no existe el directorio temporal.';
					break;
				case UPLOAD_ERR_CANT_WRITE:
					$UploadInfo->error .= 'no se pudo escribir en disco.';
					break;
				default:
					$UploadInfo->error .= 'error interno.';
					break;
			}
		}
		else {
			
			/*
			 * Validar extension del archivo
			 */
			$file_ext = strtolower( pathinfo( $file_name, PATHINFO_EXTENSION ) );
			
			if( is_array( $this->extensions ) and array_search( $file_ext, $this->extensions ) === FALSE ) {
				$UploadInfo->error = 'Extension de archivo no permitida';
			}
			else {
				
				$final_file_name = $this->getSafeFileName( ($final_file_name == NULL ? $file_name : ($final_file_name.'.'.$file_ext) ) );
				$destiny_file_path = $this->upload_path . $final_file_name;
				
				if( !@move_uploaded_file( $source_file_path, $destiny_file_path ) ) {
					$UploadInfo->error = 'No se pudo copiar el archivo a "' . $destiny_file_path . '"';
				}
				else {
					@chmod( $destiny_file_path, $this->chmod_mode );
				}
				
				$UploadInfo->final_file_name = $final_file_name;
			}
		}
		
		return $UploadInfo;
	}

	/**
	 * Devuelve un nombre de archivo normalizado y disponible si la instancia esta configurada para no sobreescribir archivos.
	 * 
	 * @param string $file_name Nombre original del archivo
	 * @return string Nombre de archivo normalizado y/o disponible
	 */
	public function getSafeFileName( $file_name ) {

		$final_file_name = $file_name;
		$file_name_parts = explode( '.', $final_file_name );
		$extension = '.' . array_pop( $file_name_parts );
		$file_name_no_ext = implode( '.', $file_name_parts );
		
		// Calcular primer corte y cortar si es necesario
		$file_name_len = strlen( $extension ) + strlen( $file_name_no_ext );
		
		if( $file_name_len > $this->file_name_max_size ) {
			$file_name_no_ext = substr( $file_name_no_ext, 0, ($this->file_name_max_size - $file_name_len) );
		}
		
		$final_file_name = $file_name_no_ext . $extension;
		
		if( !$this->overwrite ) {
			$count = 0;
			while( file_exists( $this->upload_path . $final_file_name ) ) {
				
				// Calcular corte
				$count_postfix = '_' . ++$count;
				$count_postfix_len = strlen( $count_postfix );
				$extension_len = strlen( $extension );
				$file_name_no_ext_len = strlen( $file_name_no_ext );
				$file_name_len = $count_postfix_len + $extension_len + $file_name_no_ext_len;
				
				// Cortar si es necesario
				if( $file_name_len > $this->file_name_max_size )
					$file_name_no_ext = substr( $file_name_no_ext, 0, ($this->file_name_max_size - $file_name_len) );
				
				$final_file_name = $file_name_no_ext . $count_postfix . $extension;
			} // end while
		} // end if !overwrite
		

		return $final_file_name;
	}

	/**
	 * @return array $extensions
	 */
	public function getExtensions( ) {

		return $this->extensions;
	}

	/**
	 * @return bool $overwrite
	 */
	public function getOverwrite( ) {

		return $this->overwrite;
	}

	/**
	 * @return string $input_file_name
	 */
	public function getInputFileName( ) {

		return $this->input_file_name;
	}

	/**
	 * @return string $upload_path
	 */
	public function getUploadPath( ) {

		return $this->upload_path;
	}

	/**
	 * Establece la lista de extensiones de archivo permitidas.
	 * 
	 * @param string,...
	 */
	public function setExtensions( ) {

	//	$this->extensions = array_map( 'strtolower', func_get_args( ) );
	}

	/**
	 * Si $overwrite es TRUE se sobreescribirá los archivos ya existentes en el directorio destino,
	 * si $overwrite es FALSE se generarán nombres de archivo disponibles antes de copiar.
	 * 
	 * @see FileUploader::upload()
	 * @param bool $overwrite
	 */
	public function setOverwrite( $overwrite ) {

		$this->overwrite = $overwrite;
	}

	/**
	 * Establece el nombre del input (en el formulario HTML enviado) de el(los) archivo(s) que se va(n) a copiar.
	 * 
	 * @see FileUploader::__construct()
	 * @param string $input_file_name
	 */
	public function setInputFileName( $input_file_name ) {

		$this->input_file_name = $input_file_name;
	}

	/**
	 * Establece el directorio destino en donde se copiará(n) el(los) archivo(s).
	 * 
	 * @see FileUploader::__construct()
	 * @param string $upload_path
	 */
	public function setUploadPath( $upload_path ) {

		$this->upload_path = preg_replace( '/\/+$/', '', $upload_path ) . '/';
	}

	/**
	 * @return int $file_name_max_size
	 */
	public function getFileNameMaxSize( ) {

		return $this->file_name_max_size;
	}

	/**
	 * Establece el número maximo de caracteres para los nombres de archivo,
	 * Si el nombre de archivo excede lo establecido, será cortado (sin perder la extensión),
	 * por default número máximo es 255 caracteres.
	 * 
	 * @param int $file_name_max_size
	 */
	public function setFileNameMaxSize( $file_name_max_size ) {

		$this->file_name_max_size = $file_name_max_size;
	}

	/**
	 * @return bool $ignore_no_file
	 */
	public function getIgnoreNoFile( ) {

		return $this->ignore_no_file;
	}

	/**
	 * Establece si se ignorarán los "input file" que esten vacios,
	 * si $ignore_no_file es TRUE se ignoran,
	 * si $ignore_no_file es FALSE, no se ignorarán y el (los) "input file" vacios serán
	 * marcados con un error en el (los) objeto(s) que devuelve FileUploader::upload(),
	 * por default NO son ignorados (FALSE).
	 * 
	 * @param bool $ignore_no_file
	 */
	public function setIgnoreNoFile( $ignore_no_file ) {

		$this->ignore_no_file = $ignore_no_file;
	}

	/**
	 * @return octal $chmod_mode
	 */
	public function getChmodMode( ) {

		return $this->chmod_mode;
	}

	/**
	 * Establece los permisos que se aplicaran a los archivos copiados,
	 * por default es el valor octal 0777
	 * 
	 * @param octal $chmod_mode
	 */
	public function setChmodMode( $chmod_mode ) {

		$this->chmod_mode = $chmod_mode;
	}
}